Repository navigation
feat(engine): add runtime Copilot model controls - #2246
jamesadevine with Copilot wants to merge 24 commits into
Conversation
|
Azure Pipelines: Successfully started running 1 pipeline(s). 1 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
| \x20\x20\x20\x20*[!A-Za-z0-9._:-]*)\n\ | ||
| \x20\x20\x20\x20\x20\x20echo \"ERROR: runtime Copilot model from {specific}/{ADO_AW_DEFAULT_MODEL_COPILOT} contains invalid characters. Only ASCII alphanumerics, ., _, :, and - are allowed.\" >&2\n\ | ||
| \x20\x20\x20\x20\x20\x20exit 1\n\ | ||
| \x20\x20\x20\x20\x20\x20;;\n\ |
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
|
✅ Test Quality Sentinel completed the test quality analysis.
|
|
✅ Rust Code Quality Reviewer completed the Rust code quality review. Warning Firewall blocked 1 domainThe following domain was blocked by the firewall during workflow execution:
To allow these domains, add them to the network:
allowed:
- defaults
- "releaseassets.githubusercontent.com"See Network Configuration for more information.
|
|
✅ Compiler Contract Reviewer completed the compiler contract review.
|
|
✅ PR Security Reviewer completed the security review.
|
There was a problem hiding this comment.
Security review: no regressions found
Reviewed the full diff (docs/engine.md, agentic_pipeline.rs, ado_aw_marker.rs, engine.rs) introducing runtime Copilot model pipeline-variable overrides.
- Runtime model values pass a strict allowlist (
[A-Za-z0-9._:-]) in both the bash preamble (engine.rs::runtime_model_preamble) and the aw_info.json writer, before reaching--model "$ADO_AW_EFFECTIVE_MODEL"(quoted) or JSON output — no shell/JSON injection path. - The new variable names (
ADO_AW_MODEL_AGENT_COPILOT,ADO_AW_MODEL_DETECTION_COPILOT,ADO_AW_DEFAULT_MODEL_COPILOT) are added toBLOCKED_ENV_KEYS, so untrusted workflowengine.envcannot override them. - Explicit
engine.modelstill takes precedence over runtime overrides, and no write-capable token or Stage 3 write path is touched.
No exploitable finding. Minor non-security note: the allowlist/precedence logic is duplicated between engine.rs and ado_aw_marker.rs — low risk of the two drifting out of sync over time, not blocking.
🔒 Security review by PR Security Reviewer · auto · 61.6 AIC · ⌖ 5.94 AIC · ⊞ 10.2K
Comment /review to run again
There was a problem hiding this comment.
Test Quality review
The new tests are well-scoped for verifying what strings appear in generated invocations/env/scripts, but they don't verify the runtime shell logic actually behaves correctly — every assertion is a contains() check on generated text, never an execution of the generated bash. Three findings posted inline:
copilot_detection_env's explicit-model omission path is untested (only the Agent-sidecopilot_envomission has a test), leaving an asymmetric gap.- The runtime-model precedence/validation shell logic (
runtime_model_preamble) is only string-matched, never executed, so a broken fallback order or validation regex would pass all four new invocation tests. - The precedence/validation algorithm is independently duplicated between
ado_aw_marker.rs'sado_aw_runtime_model(bash) andengine.rs'sruntime_model_preamble(bash-generating Rust) with nothing checking they agree.
None of these are merge-blocking on their own — the string-based tests do catch gross omissions — but the actual security-relevant validation logic (rejecting invalid model characters before they reach an embedded shell command) ships with zero behavioral test coverage.
🧪 Test quality analysis by Test Quality Sentinel · auto · 143.7 AIC · ⌖ 6.34 AIC · ⊞ 9.8K
Comment /review to run again
| @@ -928,6 +1005,9 @@ pub fn copilot_detection_env(engine_config: &EngineConfig) -> Result<Vec<(String | |||
| pairs.push((key.clone(), value.clone())); | |||
There was a problem hiding this comment.
copilot_detection_env gates the runtime model vars on engine_config.model().is_none() (line 1005), mirroring copilot_env's gating — but only the Agent-side omission is tested (copilot_engine_env_omits_runtime_agent_model_vars_for_explicit_model). There is no copilot_detection_env test asserting the runtime vars are omitted when an explicit Detection model is configured, so a regression that always injects ADO_AW_MODEL_DETECTION_COPILOT/ADO_AW_DEFAULT_MODEL_COPILOT even with an explicit model would ship silently.
💡 Suggested test
#[test]
fn copilot_detection_env_omits_runtime_model_vars_for_explicit_model() {
let (front_matter, _) = parse_markdown(
"---\nname: test\ndescription: test\nsafe-outputs:\n threat-detection:\n engine:\n model: detector-model\n---\n",
)
.unwrap();
let env = copilot_detection_env(&front_matter.engine).unwrap();
assert!(!env.iter().any(|(key, _)| key == ADO_AW_MODEL_DETECTION_COPILOT));
assert!(!env.iter().any(|(key, _)| key == ADO_AW_DEFAULT_MODEL_COPILOT));
}| &front_matter, | ||
| &declarations_for(&front_matter), | ||
| "/tmp/threat.md", | ||
| None, |
There was a problem hiding this comment.
The new runtime-model invocation tests only check that the generated string contains fragments (e.g. invocation.contains("*[!A-Za-z0-9._:-]*)"), invocation.contains("ADO_AW_MODEL_DETECTION_COPILOT")), never that the shell preamble in runtime_model_preamble actually resolves the precedence or rejects invalid characters when executed. This mirrors the implementation rather than testing behaviour — a change that silently breaks the fallback order (specific var, then default var) or the character-validation case guard would pass all four new tests unchanged.
💡 Suggested approach
Extract the generated runtime_model_preamble output to a temp script and execute it via bash -c with different env combinations (unset, specific-var set, default-var set, both set, invalid characters), asserting on $ADO_AW_EFFECTIVE_MODEL / exit status — similar to the pattern already used in tests/compiler_tests.rs::exercise_attached_prompt_with_pinned_copilot_cli for running generated shell.
| body: r#" | ||
| set -eo pipefail | ||
|
|
||
| ado_aw_runtime_model() { |
There was a problem hiding this comment.
ado_aw_runtime_model (bash, EMIT_AW_INFO) reimplements the same precedence/validation algorithm as runtime_model_preamble in src/engine.rs — two independent hand-written copies of the fallback-and-character-validation logic, each only string-matched in tests, with nothing asserting they agree. If one copy's regex/case-guard is tightened or loosened without updating the other, the aw_info.json metadata and the actual --model argument passed to Copilot could silently diverge (e.g. a model rejected by one path but accepted and used by the other).
💡 Suggested approach
Add a small end-to-end test that renders both EMIT_AW_INFO's script and runtime_model_preamble's output for the same ADO_AW_MODEL_AGENT_COPILOT/ADO_AW_DEFAULT_MODEL_COPILOT input combinations (valid, invalid-chars, $(VAR) literal, empty) and checks their outcomes match — or, better, factor the character-validation case pattern into one shared constant/fragment so a change to the allowed character set can't be made in only one place.
There was a problem hiding this comment.
Rust review: COMMENT
Note: the rust-critic sub-agent failed to return output (model access error) on two attempts; findings below are from my own pass only.
Themes
- One
pubmethod (invocation_with_config) is left as dead code behind#[allow(dead_code)]after this PR rewires its only caller todetection_invocation_with_config. - The runtime-model character validation regex/case is duplicated across three locations (shell
EMIT_AW_INFO, shellruntime_model_preamble, Rustvalidate_model_name), risking silent drift.
Both are maintainability concerns, not merge-blockers — the actual injection-prevention logic is sound (validated before use in all three copies), tests pass (cargo test copilot_), and the documented precedence order matches the implementation.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
releaseassets.githubusercontent.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "releaseassets.githubusercontent.com"See Network Configuration for more information.
🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 128.9 AIC · ⌖ 1.96 AIC · ⊞ 10.1K
Comment /review to run again
| /// | ||
| /// Retained for callers that need a plain invocation with no role-specific | ||
| /// runtime model controls. | ||
| #[allow(dead_code)] |
There was a problem hiding this comment.
invocation_with_config is now dead code: after this PR its only caller was replaced by detection_invocation_with_config, and a repo-wide grep shows zero remaining call sites. It is kept alive solely by #[allow(dead_code)] on a pub method, so the compiler warning that would normally catch this is suppressed rather than fixed.
💡 Suggested fix
Either delete invocation_with_config entirely (it duplicates detection_invocation_with_config with runtime_model_role: None, so a caller could just pass None if this ever becomes needed again), or remove the #[allow(dead_code)] and let the compiler flag it if it is truly meant to be temporarily retained. Leaving pub dead code with a suppressed lint is a maintenance trap: a future refactor can silently rot this path without any signal.
| body: r#" | ||
| set -eo pipefail | ||
|
|
||
| ado_aw_runtime_model() { |
There was a problem hiding this comment.
The runtime-model character-class validation (*[!A-Za-z0-9._:-]* in bash) is now implemented three times: here in the EMIT_AW_INFO shell script, again in runtime_model_preamble (src/engine.rs), and as the Rust-side validate_model_name for explicit engine.model. The three copies must stay byte-for-byte in sync or a future edit to one silently diverges from the others, re-opening the injection surface this validation exists to close.
💡 Suggested fix
Extract the bash validation into a single fragment/binding shared by both EMIT_AW_INFO and runtime_model_preamble, or at minimum add a comment cross-referencing all three call sites so a reviewer touching one is prompted to check the others. A regression test asserting the two shell-emitted validation blocks are textually identical would also catch drift automatically.
There was a problem hiding this comment.
Compiler-contract review focused on drift and typed-contract invariants (not general Rust style, covered by other reviewers).
Checked and clean:
- Front-matter grammar: no new/renamed fields in
src/compile/types.rs. BLOCKED_ENV_KEYScorrectly extended with the three newADO_AW_MODEL_*runtime variables, and a test (copilot_engine_env_rejects_user_runtime_model_var_override) provesengine.envcannot shadow them.EMIT_AW_INFO/RUN_AGENTshell changes stay inside theShellScript/Binding/shell_script!framework — newexternals:entries are declared for the three runtime model vars, andENFORCE_BASH_LINT=1 cargo test --bin ado-aw compile::shell/--test bash_lint_testsboth pass locally with the change in place.docs/engine.mddocuments the newRuntime model controlssection, precedence order, and the Detection-engine interaction — matches the code.- No public
PipelineSummary/GraphSummaryfields touched;aw_info.json'smodel/detection_modelkeys are optional strings already handled bysrc/audit/model.rs. - No ado-script bundle sources or codegen inputs (
filter_ir.rs,Fact) changed, so the bundle/codegen drift checks don't apply here.
One finding posted inline on src/engine.rs: the renamed detection_invocation_with_config orphaned the old invocation_with_config, which is now dead code hidden behind #[allow(dead_code)] instead of being deleted.
🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 200.2 AIC · ⌖ 1.92 AIC · ⊞ 11.3K
Comment /review to run again
| /// Retained for callers that need a plain invocation with no role-specific | ||
| /// runtime model controls. | ||
| #[allow(dead_code)] | ||
| pub fn invocation_with_config( |
There was a problem hiding this comment.
invocation_with_config is now unreadable dead code kept alive with #[allow(dead_code)]
Removing the #[allow(dead_code)] and rebuilding produces warning: methods ... invocation_with_config ... never used — the only prior caller was rewired to detection_invocation_with_config in this same PR (src/compile/agentic_pipeline.rs), so this method has zero remaining call sites anywhere in the tree.
💡 Why this matters / fix
Suppressing the warning instead of deleting the method hides genuinely orphaned public API from cargo build/clippy going forward — the next contributor who edits invocation_with_config gets no compiler signal that it is unreachable, and a real future regression in it would go unnoticed since nothing exercises it.
Fix: delete invocation_with_config (and the now-unnecessary #[allow(dead_code)]) unless there is a concrete near-term caller planned; if one is coming in a follow-up PR, say so explicitly instead of the generic "retained for callers that need a plain invocation" note, which as written applies to no one.
Verified via: sed -i 's/#\[allow(dead_code)\]//' src/engine.rs && cargo build --bin ado-aw → emits the never-used warning.
|
@copilot Address feedback |
Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
Superseded by updated review from same workflow.
There was a problem hiding this comment.
Compiler Contract Review
This is a large, well-executed refactor of the Copilot invocation path (shell-string --model/args to a typed JSON request/prepared/result protocol shared by Rust and a new copilot-controller/copilot-runner TS bundle pair). I checked for the contract-drift classes this review focuses on and found none outstanding:
- Bundle registry /
ado_bundle.rs:CopilotControllerandCopilotRunnerare registered inBundle::ALL, have path entries, and are correctly included in the no-ADO-auth set (GitHub/local-only auth, not ADO REST). - Codegen drift: no changes to
filter_ir.rs/Factin this diff, sotypes.gen.ts/fact-catalog.gen.jsoncorrectly did not move. - Lock drift: all
.github/workflows/*.mdchanges have matching.lock.ymldiffs. - AGENTS.md / docs sync: the new
copilot-shared/controller/runnerdirectories are documented in the architecture tree and indocs/ado-script.md's bundle list;docs/engine.mdgained a full "Runtime model controls" section describing the newADO_AW_MODEL_*variables and precedence;docs/audit.mddocuments the newaw_info.jsonfields (modelvsdetection_model, requested-vs-observed). All in sync with the code. - Generated shell:
RUN_AGENT/RUN_THREAT_ANALYSISroute the new trusted-preflight/model-result logic entirely through typedBindings and a registeredphases:/fragments:shell script (APPEND_AW_INFO_FIELD,COPILOT_TRUSTED_PREFLIGHT) -- noformat!-built shell, no raw credential bindings. - Secure newtypes: the new
is_valid_command_pathvalidation (rejecting./../empty/doubled-slash segments) andvalidate_model_nameare mirrored exactly inprotocol.ts'sCOMMAND_PATTERN/MODEL_PATTERN+hasSafePathSegments, so the Rust and TS halves of this security boundary cannot drift. - Prior review feedback: the previously-flagged dead
invocation_with_configmethod and triplicated model-precedence/validation logic are gone in this diff -- functionality was consolidated intoinvocation_request_with_config/validate_model_nameand the sharedAPPEND_AW_INFO_FIELDphase.
I built the compiler (cargo build), ran the full Rust suite (cargo test, all passing) plus the targeted engine::/validate:: modules and runtime_model_controls_compile_across_all_targets, and ran the new copilot-controller/copilot-runner/copilot-shared vitest suites (43/43 passing) and npm run typecheck (clean). No findings to post inline.
Warning
Firewall blocked 2 domains
The following domains were blocked by the firewall during workflow execution:
releaseassets.githubusercontent.comspsprodeus21.vssps.visualstudio.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "releaseassets.githubusercontent.com"
- "spsprodeus21.vssps.visualstudio.com"See Network Configuration for more information.
🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 142.1 AIC · ⌖ 1.91 AIC · ⊞ 12.5K
Comment /review to run again
There was a problem hiding this comment.
TypeScript review — scripts/ado-script/ (new copilot-controller/copilot-runner/copilot-shared protocol)
Reviewed the new copilot invocation split (protocol.ts, copilot-controller/index.ts, copilot-runner/index.ts) plus their tests. ts-critic ran in the background and returned 5 findings; after triage, 2 were genuine and merge-worthy and are posted inline below, the rest (prompt-size guard, swallowed JSON.parse detail, broad argv-path trust) are advisory nitpicks not worth blocking on.
Strengths:
- The request/prepared/result schema validation in
protocol.tsis thorough — unknown-key rejection, NUL-byte checks, path/command allowlisting via regex, and noany/non-null-assertion type-safety holes on externally parsed JSON. - Signal forwarding and exit-code mapping in
copilot-runneris carefully handled (settleguard against double-resolve,offcleanup of listeners). - Good test coverage of model precedence (
resolveRequestedModel), unresolved ADO macro detection, and the role-mismatch / missing-result paths incopilot-controller.
Findings (inline):
copilot-controller/index.ts:27— write-ordering bug:result.jsonis written beforeprepared.json, which can leave a stale/misleading result visible to the trusted host after a crash between the two writes, contradicting the documented "commit result before exposing prepared" intent.copilot-runner/index.ts:50— self-removal ofpreparedPath/runnerPathhappens beforespawnis confirmed to succeed, so aspawnerrorevent (binary missing, perms) loses the invocation audit trail that would otherwise help debug the failure.
Neither is merge-blocking on its own (both are narrow-window failure modes), but #1 in particular undermines the stated trust-boundary guarantee of the two-file protocol, so I'd like it addressed or the design rationale clarified before merge.
🟦 TypeScript code quality review by TypeScript Code Quality Reviewer · auto · 154.9 AIC · ⌖ 8.49 AIC · ⊞ 11.3K
Comment /review to run again
There was a problem hiding this comment.
Security review — no blocking findings
Reviewed the diff against the threat model (compiler → generated-YAML injection, Stage 1/3 token isolation, AWF sandbox boundary, validation strength).
Net assessment: security-neutral to positive. The headline change — replacing the shell-embedded --prompt="$(cat ...)" engine invocation string with a copilot-controller/copilot-runner protocol that spawns Copilot via child_process.spawn(command, argv[]) — removes a shell-reinterpretation step for prompt/arg content and is a hardening, not a regression.
Specifics checked:
is_valid_command_path(Rust) andisSafeCommand/hasSafePathSegments(TS protocol.ts) are tightened together: both now reject./..segments, empty segments, and trailing slashes — stricter than the prior character-class-only check.Bundle::CopilotController/Bundle::CopilotRunnerare correctly placed in the no-SYSTEM_ACCESSTOKENbucket inado_bundle.rs(no ADO REST calls), preserving least privilege.- The trusted controller/result files live under
$(Agent.TempDirectory), outside AWF's automatic/tmpmount (perdocs/network.md), matching the existingazure-wif-refreshisolation pattern. The controller source copied into the sandbox-visible path is deleted before AWF starts, and AWF hasn't executed yet at that point, so there's no attacker-controlled race window. ADO_AW_MODEL_AGENT_COPILOT/ADO_AW_MODEL_DETECTION_COPILOT/ADO_AW_DEFAULT_MODEL_COPILOT/COPILOT_MODELwere added toBLOCKED_ENV_KEYS, and a new check rejects--modelinengine.args, closing the obvious bypass of the new precedence chain.buildChildEnvironmentstrips the runtime-override vars before spawning the real Copilot process.validate_model_name/MODEL_PATTERNare kept in sync between the Rust compiler and the TS protocol module.
Didn't find a traceable injection, token-exposure, or weakened-validation path introduced by this diff, so no inline comments. (Pre-existing review threads already cover the DRY/dead-code concerns in engine.rs and ado_aw_marker.rs — out of scope for a security pass.)
🔒 Security review by PR Security Reviewer · auto · 157.6 AIC · ⌖ 2.72 AIC · ⊞ 11.4K
Comment /review to run again
There was a problem hiding this comment.
Verdict: COMMENT — no new merge-blocking defects in the Rust changes
Reviewed the Rust-only portion of the diff (src/audit/**, src/compile/ado_bundle.rs, src/compile/agentic_pipeline.rs, src/compile/extensions/{ado_aw_marker,ado_script,exec_context,mod}.rs, src/compile/types.rs, src/engine.rs, src/validate.rs, tests/compiler_tests.rs).
rust-critic returned three findings; I verified each:
- HIGH — "untrusted model name interpolated into JSON via
printfwithout escaping" (ado_aw_marker.rs): verified false positive.REQUESTED_MODELis validated against^[A-Za-z0-9._:-]+$(validate_model_nameinsrc/engine.rs, mirrored byMODEL_PATTERNinscripts/ado-script/src/copilot-shared/protocol.ts) both when the request is built and again whenparseInvocationResultre-parses the controller's result file. A quote or backslash can never reach theprintfcall. - LOW — blocking
Path::is_file()inside asyncfind_analyzed_outputs_dir: this mirrors the pre-existing identical pattern inotel.rs::locate_agent_output_file(not introduced by this PR) — consistent with current codebase convention, not a regression. - LOW —
is_valid_command_pathhas no explicit length cap: defense-in-depth suggestion only; no demonstrated exploit path since the character allowlist already excludes shell metacharacters. Not blocking.
My own pass over error handling, casts, cross-platform paths, determinism (HashMap/sort() usage in copilot_provider_env/copilot_detection_env), and the new set -eo pipefail / set +e exit-code plumbing in RUN_AGENT/RUN_THREAT_ANALYSIS didn't surface additional issues — the fail-closed ordering (trusted prepare runs under set -e before set +e) is correct and is exercised by the new tests. cargo build is clean.
The genuinely interesting architectural risks here — triplicated runtime-model precedence/validation logic across engine.rs, the EMIT_AW_INFO bash script, and the TS protocol, the copilot_detection_env/copilot_engine_env test-coverage asymmetry, and the invocation_with_config dead-code path — are already tracked in existing review threads on this PR, so I'm not duplicating them.
🦀 Rust code quality review by Rust Code Quality Reviewer · auto · 401.6 AIC · ⌖ 2.27 AIC · ⊞ 11.3K
Comment /review to run again
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
|
/azp run ado-aw candidate compiler smoke |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
|
/azp run ado-aw candidate compiler smoke |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
|
/azp run ado-aw candidate compiler smoke |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
|
/azp run ado-aw candidate compiler smoke |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 948a095f-e847-4d07-9f93-19a4bab86459
|
/azp run ado-aw candidate compiler smoke |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
/azp run ado-aw candidate compiler smoke |
|
/review |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
There was a problem hiding this comment.
Compiler-contract review
Focused on drift (codegen/lock/smoke), front-matter/safe-output/IR schema contracts, generated-shell discipline, and docs sync. Did not duplicate the Rust/TS quality findings already in pr-review-comments.json (the copilot_detection_env/copilot_env gating-test gap, the triplicated validation-algorithm note, and the shallow contains() invocation tests — all still accurate as of this diff and worth addressing).
No new contract-drift findings. Specifically checked and clean:
- Codegen/lock/smoke drift: no
filter_ir.rs/Factchanges in this diff, sotypes.gen.ts/fact-catalog.gen.jsoncorrectly didn't move. No.github/workflows/*.mdchanged without its.lock.yml.tests/smoke/runtime-model-*.mdare markdown-only sources wired through onecases.jsonentry each (REGISTERED.mdupdated to explain theagentic-lane queue-time variable) — matches the lane/registration design, no reintroduced per-case ADO definitions. - Front-matter grammar: no new/removed
FrontMatterfields intypes.rs(only two internal helper methods removed, both now dead after theexec_context_*_active/safe_outputs_summary_active/etc. flag removal) — no codemod needed. - Duplicated-activation-predicate removal: this PR actually resolves a drift risk rather than introducing one — the
pr_contributor_will_activate/manual_contributor_will_activate/etc. "MUST stay in lock-step" helper pairs betweenexec_context/mod.rsandAdoScriptExtensionare deleted wholesale, replaced by always-staging the Copilot controller/runner bundles in Agent prepare. Good simplification. - New
Bundle::CopilotController/CopilotRunner: correctly added toado_bundle.rs's "no ADO bearer" exemption list (GitHub/no-REST bundles) — consistent with their GitHub App / credential-free nature. - Generated shell:
RUN_AGENT/RUN_THREAT_ANALYSIS/RECORD_DETECTION_RUNTIME_MODELall go throughShellScriptwith typed bindings; the JSON invocation request is correctly passed viaBinding::document(not raw interpolation), and the sharedAPPEND_AW_INFO_FIELDphase is reused viaphase_body()rather than re-implemented at each call site — avoids yet another drift copy. - Docs sync:
AGENTS.mdarchitecture tree,docs/ado-script.md,docs/engine.md(new "Runtime model controls" section), anddocs/audit.md(newaw_info.model/detection_modelsemantics + overlay behavior) are all updated and match the implementation (src/audit/model.rs,cli.rs'smerge_detection_aw_info,otel.rs's observed-vs-requested model precedence) — verified with the corresponding new tests in each file. - Trusted/untrusted boundary: the new
copilot-controller/copilot-runner/copilot-sharedsplit (prepare on host, run-only in sandbox, strict schema-v2 validation withrejectUnknown, atomic writes, self-removal of the prepared doc before exec) matches theAGENTS.mdsecurity note added in this same PR about never trusting/tmpartifacts across the AWF boundary — the controller copy and result live under$(Agent.TempDirectory), outside AWF's host/tmpmount, consistent with that rule.
Nothing here rises to REQUEST_CHANGES on compiler-contract grounds; the gating/test-coverage gaps already flagged by prior reviews remain the operative action items.
🏗️ Compiler contract review by Compiler Contract Reviewer · auto · 129.7 AIC · ⌖ 18.1 AIC · ⊞ 12.5K
Comment /review to run again
There was a problem hiding this comment.
Security review: no regressions found
This diff is security-neutral to positive. The headline change — replacing the shell-interpolated --prompt="$(cat path)" invocation (embedded inside a single-quoted AWF -- command string) with a trusted copilot-controller/copilot-runner protocol that reads the prompt file and passes it as a literal spawn() argv element (no shell: true) — removes a shell-expansion surface for prompt/model content rather than introducing one.
Specific checks performed against the threat model in this repo's docs:
- Injection into generated pipelines: the new
INVOCATION_REQUESTdocument is built viaserde_json::to_stringand embedded through the existingBinding::documentheredoc binding (which asserts no credential content and no delimiter collision) — not string concatenation.command,prompt_path, andmcp_config_pathare compiler-controlled, not operator/PR-controlled. - Weakened validation: none found.
is_valid_command_path(src/validate.rs) got stricter (now rejects.., empty segments, trailing slash, bare relative paths likebin/copilot), and a newvalidate_model_name/MODEL_PATTERNenforces^[A-Za-z0-9._:-]+$consistently in bothsrc/engine.rsandscripts/ado-script/src/copilot-shared/protocol.ts.protocol.tsadditionally rejects unknown JSON fields (rejectUnknown) and NUL bytes in every parsed document. - Token scope /
ado_bundle.rs:CopilotController/CopilotRunnerare added to the bundle registry and correctly placed alongside the no-ADO-auth bundles (GithubAppToken, ApprovalSummary, etc.) rather than theSYSTEM_ACCESSTOKEN-projected set — consistent with them never making ADO REST calls. - Trust boundary: the controller (trusted, runs outside AWF,
install -m 0500) writes the sandbox-visible invocation document and is then deleted (rm -f "$COPILOT_CONTROLLER_SOURCE_PATH") before AWF starts; the runner (untrusted, runs inside AWF) only reads the prepared document, which has already been schema-validated and had the compiler-forbiddenADO_AW_MODEL_*/COPILOT_MODELenv keys stripped viabuildChildEnvironment. Prepared files are atomically written (writeJsonAtomic, mode 0600, rename-after-write). - Network/allowlist: no new domains added.
- Safe-output integrity: unaffected by this diff.
No merge-blocking findings. The existing unresolved github-actions[bot] review threads (duplicated validation logic across runtime_model_preamble/ado_aw_runtime_model/protocol.ts, and the invocation_with_config dead-code question) are code-quality/maintainability concerns, not security regressions, and are already tracked — not duplicating them here.
🔒 Security review by PR Security Reviewer · auto · 115.8 AIC · ⌖ 2.02 AIC · ⊞ 11.4K
Comment /review to run again
There was a problem hiding this comment.
Test Quality Review
This PR's test coverage is thorough. The new copilot-shared/protocol.ts module is exhaustively tested (strict schema validation, every rejection path, atomic-write behavior, argv/env construction including adversarial prompt content). copilot-controller/copilot-runner each cover their success, error, and signal-forwarding paths, including the security-relevant "does not expose sandbox run mode" / "does not expose controller modes" boundary tests. The engine.rs runtime-model precedence and gating logic — previously flagged in earlier review passes as having an untested Detection-side gap and weak substring-only invocation assertions — now has symmetric Agent/Detection tests with structured field assertions (copilot_detection_env_omits_runtime_model_vars_for_explicit_model, copilot_invocation_request_defers_runtime_model_resolution, etc.), and the previously-flagged invocation_with_config dead code no longer exists. The Rust ado_aw_marker.rs metadata-merge logic is well covered for value/key-collision edge cases (e.g. append_aw_info_field_does_not_confuse_value_with_model_key) and failure-without-exit-propagation. The new smoke cases follow the two-file convention correctly.
I found one minor untested branch (inline comment): the audit console renderer's new requested-vs-observed model split only has tests for the "differ" and "equal" branches, not the "observed model absent" branch described as a non-fatal case in the PR summary.
Non-blocking — approving the test quality here.
🧪 Test quality analysis by Test Quality Sentinel · auto · 213.9 AIC · ⌖ 1.7 AIC · ⊞ 11K
Comment /review to run again
There was a problem hiding this comment.
TypeScript review (scripts/ado-script/)
Reviewed the new Copilot invocation protocol (copilot-shared/protocol.ts, copilot-controller, copilot-runner) and the compiler-smoke-e2e queue-variable/model-assertion additions.
No high-confidence defects found:
- Error handling is sound:
copilot-runnerforwards SIGINT/SIGTERM/SIGHUP with proper cleanup, settles exactly once, and fails closed when self-removal throws. resolveRequestedModelvalidates runtime model values against a strict character class and never includes the rejected value in its thrown message (verified by test).- Path/command validation (
hasSafePathSegments,isSafeCommand) correctly rejects traversal segments, relative paths, and trailing slashes. writeJsonAtomicwrites with0o600and renames, avoiding partial-write exposure.- New branches in
signals.ts/cases.ts/ado-rest.tsare covered by matching test cases.
A second pass via a dedicated hostile-review sub-agent independently reached the same conclusion (no findings).
No inline comments to post.
🟦 TypeScript code quality review by TypeScript Code Quality Reviewer · auto · 235.3 AIC · ⌖ 2.62 AIC · ⊞ 11.3K
Comment /review to run again
Summary
ADO operators could not switch Agent or Detection Copilot models during an outage without editing workflow markdown and recompiling. This adds runtime model controls through Azure DevOps YAML variables, UI variables, and variable groups while preserving explicit front-matter model precedence.
engine.model/ effective Detection modelADO_AW_MODEL_AGENT_COPILOTorADO_AW_MODEL_DETECTION_COPILOTADO_AW_DEFAULT_MODEL_COPILOTBundled Copilot invocation
copilot-invoker.jsprotocolaw_info.jsonado-script.zipin every Agent and enabled Detection job through existing release/feed/pipeline-artifact supply-chain pathsSafe runtime plumbing
enventries, avoiding macro expansion inside generated Bashtask.setvariablesteps are honoredCOPILOT_MODELonly in the child environmentengine.args --modelandengine.env.COPILOT_MODELconfigurationAccurate run metadata
aw_info.jsonTest plan
cargo testcargo clippy --all-targets -- -D warningscargo check --all-targetscargo test --bin ado-aw compile::shellcargo test --test generated_shell_guardcargo test --test bash_lint_testsnpm --prefix scripts/ado-script test -- --pool=forks --fileParallelism=falsenpm --prefix scripts/ado-script run typechecknpm --prefix scripts/ado-script run buildnpm --prefix scripts/ado-script run test:smoke